diff --git a/puppetboard/app.py b/puppetboard/app.py
index 0ac6db7..141fe5f 100644
--- a/puppetboard/app.py
+++ b/puppetboard/app.py
@@ -1,313 +1,319 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import os
import logging
import collections
-import urllib
+try:
+ from urllib import unquote
+except ImportError:
+ from urllib.parse import unquote
from datetime import datetime, timedelta
+from multiprocessing.dummy import Pool as ThreadPool
from flask import (
Flask, render_template, abort, url_for,
Response, stream_with_context, redirect,
request
)
from pypuppetdb import connect
from puppetboard.forms import QueryForm
from puppetboard.utils import (
get_or_abort, yield_or_stop,
ten_reports, jsonprint
)
app = Flask(__name__)
app.config.from_object('puppetboard.default_settings')
app.config.from_envvar('PUPPETBOARD_SETTINGS', silent=True)
app.secret_key = os.urandom(24)
app.jinja_env.filters['jsonprint'] = jsonprint
puppetdb = connect(
api_version=3,
host=app.config['PUPPETDB_HOST'],
port=app.config['PUPPETDB_PORT'],
ssl_verify=app.config['PUPPETDB_SSL_VERIFY'],
ssl_key=app.config['PUPPETDB_KEY'],
ssl_cert=app.config['PUPPETDB_CERT'],
timeout=app.config['PUPPETDB_TIMEOUT'],)
numeric_level = getattr(logging, app.config['LOGLEVEL'].upper(), None)
if not isinstance(numeric_level, int):
raise ValueError('Invalid log level: %s' % loglevel)
logging.basicConfig(level=numeric_level)
log = logging.getLogger(__name__)
def stream_template(template_name, **context):
app.update_template_context(context)
t = app.jinja_env.get_template(template_name)
rv = t.stream(context)
rv.enable_buffering(5)
return rv
@app.errorhandler(400)
def bad_request(e):
return render_template('400.html'), 400
@app.errorhandler(403)
def bad_request(e):
return render_template('403.html'), 400
@app.errorhandler(404)
def not_found(e):
return render_template('404.html'), 404
@app.errorhandler(412)
def precond_failed(e):
"""We're slightly abusing 412 to handle missing features
depending on the API version."""
return render_template('412.html'), 412
@app.errorhandler(500)
def server_error(e):
return render_template('500.html'), 500
@app.route('/')
def index():
"""This view generates the index page and displays a set of metrics and
latest reports on nodes fetched from PuppetDB.
"""
# TODO: Would be great if we could parallelize this somehow, doing these
# requests in sequence is rather pointless.
prefix = 'com.puppetlabs.puppetdb.query.population'
- num_nodes = get_or_abort(
- puppetdb.metric,
- "{0}{1}".format(prefix, ':type=default,name=num-nodes'))
- num_resources = get_or_abort(
- puppetdb.metric,
- "{0}{1}".format(prefix, ':type=default,name=num-resources'))
- avg_resources_node = get_or_abort(
- puppetdb.metric,
- "{0}{1}".format(prefix, ':type=default,name=avg-resources-per-node'))
+ pool = ThreadPool()
+ endpoints = [
+ "{0}{1}".format(prefix, ':type=default,name=num-nodes'),
+ "{0}{1}".format(prefix, ':type=default,name=num-resources'),
+ "{0}{1}".format(prefix, ':type=default,name=avg-resources-per-node'),
+ ]
+ fetched_metrics = pool.map(puppetdb.metric, endpoints)
metrics = {
- 'num_nodes': num_nodes['Value'],
- 'num_resources': num_resources['Value'],
- 'avg_resources_node': "{0:10.0f}".format(avg_resources_node['Value']),
+ 'num_nodes': fetched_metrics[0]['Value'],
+ 'num_resources': fetched_metrics[1]['Value'],
+ 'avg_resources_node': "{0:10.0f}".format(fetched_metrics[2]['Value']),
}
nodes = puppetdb.nodes(
unreported=app.config['UNRESPONSIVE_HOURS'],
with_status=True)
nodes_overview = []
stats = {
'changed': 0,
'unchanged': 0,
'failed': 0,
'unreported': 0,
}
for node in nodes:
if node.status == 'unreported':
stats['unreported'] += 1
elif node.status == 'changed':
stats['changed'] += 1
elif node.status == 'failed':
stats['failed'] += 1
else:
stats['unchanged'] += 1
if node.status != 'unchanged':
nodes_overview.append(node)
return render_template(
'index.html',
metrics=metrics,
nodes=nodes_overview,
stats=stats
)
@app.route('/nodes')
def nodes():
"""Fetch all (active) nodes from PuppetDB and stream a table displaying
those nodes.
Downside of the streaming aproach is that since we've already sent our
headers we can't abort the request if we detect an error. Because of this
we'll end up with an empty table instead because of how yield_or_stop
works. Once pagination is in place we can change this but we'll need to
provide a search feature instead.
"""
status_arg = request.args.get('status', '')
nodelist = puppetdb.nodes(
unreported=app.config['UNRESPONSIVE_HOURS'],
with_status=True)
nodes = []
for node in yield_or_stop(nodelist):
if status_arg:
if node.status == status_arg:
nodes.append(node)
else:
nodes.append(node)
return Response(stream_with_context(
stream_template('nodes.html', nodes=nodes)))
@app.route('/node/')
def node(node_name):
"""Display a dashboard for a node showing as much data as we have on that
node. This includes facts and reports but not Resources as that is too
heavy to do within a single request.
"""
node = get_or_abort(puppetdb.node, node_name)
facts = node.facts()
reports = ten_reports(node.reports())
return render_template(
'node.html',
node=node,
facts=yield_or_stop(facts),
reports=yield_or_stop(reports))
@app.route('/reports')
def reports():
"""Doesn't do much yet but is meant to show something like the reports of
the last half our, something like that."""
return render_template('reports.html')
@app.route('/reports/')
def reports_node(node):
"""Fetches all reports for a node and processes them eventually rendering
a table displaying those reports."""
reports = ten_reports(yield_or_stop(
puppetdb.reports('["=", "certname", "{0}"]'.format(node))))
return render_template(
'reports_node.html',
reports=reports,
nodename=node)
@app.route('/report/latest/')
def report_latest(node_name):
"""Redirect to the latest report of a given node. This is a workaround
as long as PuppetDB can't filter reports for latest-report? field. This
feature has been requested: http://projects.puppetlabs.com/issues/21554
"""
node = get_or_abort(puppetdb.node, node_name)
reports = get_or_abort(puppetdb._query, 'reports',
query='["=","certname","{0}"]'.format(node_name),
limit=1)
if len(reports) > 0:
report = reports[0]['hash']
return redirect(url_for('report', node=node_name, report_id=report))
else:
abort(404)
@app.route('/report//')
def report(node, report_id):
"""Displays a single report including all the events associated with that
report and their status.
"""
reports = puppetdb.reports('["=", "certname", "{0}"]'.format(node))
for report in reports:
if report.hash_ == report_id:
events = puppetdb.events('["=", "report", "{0}"]'.format(
report.hash_))
return render_template(
'report.html',
report=report,
events=yield_or_stop(events))
else:
abort(404)
@app.route('/facts')
def facts():
"""Displays an alphabetical list of all facts currently known to
PuppetDB."""
facts_dict = collections.defaultdict(list)
facts = get_or_abort(puppetdb.fact_names)
for fact in facts:
letter = fact[0].upper()
letter_list = facts_dict[letter]
letter_list.append(fact)
facts_dict[letter] = letter_list
sorted_facts_dict = sorted(facts_dict.items())
return render_template('facts.html', facts_dict=sorted_facts_dict)
@app.route('/fact/')
def fact(fact):
"""Fetches the specific fact from PuppetDB and displays its value per
node for which this fact is known."""
# we can only consume the generator once, lists can be doubly consumed
# om nom nom
localfacts = [f for f in yield_or_stop(puppetdb.facts(name=fact))]
return Response(stream_with_context(stream_template(
'fact.html',
name=fact,
facts=localfacts)))
@app.route('/fact//')
def fact_value(fact, value):
"""On asking for fact/value get all nodes with that fact."""
facts = get_or_abort(puppetdb.facts, fact, value)
localfacts = [f for f in yield_or_stop(facts)]
return render_template(
'fact.html',
name=fact,
value=value,
facts=localfacts)
@app.route('/query', methods=('GET', 'POST'))
def query():
"""Allows to execute raw, user created querries against PuppetDB. This is
currently highly experimental and explodes in interesting ways since none
of the possible exceptions are being handled just yet. This will return
the JSON of the response or a message telling you what whent wrong /
why nothing was returned."""
if app.config['ENABLE_QUERY']:
form = QueryForm()
if form.validate_on_submit():
+ if form.query.data[0] == '[':
+ query = form.query.data
+ else:
+ query = '[{0}]'.format(form.query.data)
result = get_or_abort(
puppetdb._query,
form.endpoints.data,
- query='[{0}]'.format(form.query.data))
+ query=query)
return render_template('query.html', form=form, result=result)
return render_template('query.html', form=form)
else:
log.warn('Access to query interface disabled by administrator..')
abort(403)
@app.route('/metrics')
def metrics():
metrics = get_or_abort(puppetdb._query, 'metrics', path='mbeans')
- for key, value in metrics.iteritems():
+ for key, value in metrics.items():
metrics[key] = value.split('/')[3]
return render_template('metrics.html', metrics=sorted(metrics.items()))
@app.route('/metric/')
def metric(metric):
- name = urllib.unquote(metric)
+ name = unquote(metric)
metric = puppetdb.metric(metric)
return render_template(
'metric.html',
name=name,
metric=sorted(metric.items()))
diff --git a/puppetboard/static/css/puppetboard.css b/puppetboard/static/css/puppetboard.css
index 46d002f..01b8e63 100644
--- a/puppetboard/static/css/puppetboard.css
+++ b/puppetboard/static/css/puppetboard.css
@@ -1,104 +1,65 @@
body {
- padding-top: 60px;
-}
-th.headerSortUp {
- position: relative
-}
-th.headerSortDown {
- position: relative
-}
-th.header {
- position: relative
-}
-th.header:after {
- content: "\f0dc";
- font-family: FontAwesome;
- font-style: normal;
- font-weight: normal;
- text-decoration: inherit;
- color: #000;
- font-size: 18px;
- padding-right: 0.5em;
- float:right;
-}
-th.headerSortUp:after {
- content: "\f0de";
- font-family: FontAwesome;
- font-style: normal;
- font-weight: normal;
- text-decoration: inherit;
- color: #000;
- font-size: 18px;
- padding-right: 0.5em;
- float:right;
-}
-th.headerSortDown:after {
- content: "\f0dd";
- font-family: FontAwesome;
- font-style: normal;
- font-weight: normal;
- text-decoration: inherit;
- color: #000;
- font-size: 18px;
- padding-right: 0.5em;
- float:right;
-}
-.stat {
- margin-bottom: 40px;
-}
-.navbar .brand:hover {
- color: #fff;
-}
-.table tbody tr.error>td {
- background-color: #f2dede;
-}
-h1.error {
- color: rgb(223, 46, 27);
-}
-h1.success {
- color: #18BC9C;
-}
-h1.noop {
- color:#aaa;
-}
-tr.event {
- cursor: pointer;
-}
-td.message {
- padding: 0;
- border: 0;
- background-color: #FFFFE9;
-}
-div[id^='message-event'] {
- display: none;
- padding: 4px 15px 4px 15px;
-}
-.label-count {
- width:25px;
- text-align:center;
-}
-.label-time {
- width:73px;
- text-align:center;
-}
-.label-status {
- width:100px;
- text-align:center;
-}
-.label-nothing {
- background-color:#ddd;
- color:#ddd;
-}
-.label.label-failed {
- background-color: rgb(231, 76, 60);
-}
-.label.label-changed {
- background-color: rgb(24, 188, 156);
-}
-.label.label-unreported {
- background-color: rgb(231, 76, 60);
- background-color: rgb(129, 145, 146);
-}
-.btn-lastreport {
- width:100px;
+ margin: 0;
+ font-family: "Open Sans", sans-serif;
+}
+
+a {
+ color: #2C3E50;
+ text-decoration: none;
+}
+
+h1.ui.header.no-margin-bottom {
+ margin-bottom: 0;
+}
+
+.tablesorter-header-inner {
+ float: left;
+}
+
+th.tablesorter-headerAsc::after {
+ content: '\25b4' !important;
+ float: right;
+}
+
+th.tablesorter-headerDesc::after {
+ content: '\25be' !important;
+ float: right;
+}
+
+.ui.grid.padding-bottom {
+ padding-bottom: 40px !important;
+}
+
+.status {
+ width: 47%;
+ text-align: center;
+ display: block;
+}
+.count {
+ width: 21%;
+ text-align: center;
+ display: block;
+}
+
+.no-margin-top {
+ margin-top: -35px !important;
+}
+
+.absolute {
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ background: #E8E8E8;
+}
+
+.absolute div {
+ padding: 1em;
+}
+
+.ui.menu.darkblue {
+ background-color:#2C3E50;
+}
+
+.ui.darkblue.header, i.darkblue {
+ color:#2C3E50;
}
diff --git a/puppetboard/static/js/tablesort.min.js b/puppetboard/static/js/tablesort.min.js
new file mode 100644
index 0000000..ab17511
--- /dev/null
+++ b/puppetboard/static/js/tablesort.min.js
@@ -0,0 +1,9 @@
+/*
+ A simple, lightweight jQuery plugin for creating sortable tables.
+ https://github.com/kylefox/jquery-tablesort
+ Version 0.0.2
+*/
+$(function(){var a=window.jQuery;a.tablesort=function(d,c){var e=this;this.$table=d;this.$thead=this.$table.find("thead");this.settings=a.extend({},a.tablesort.defaults,c);this.$table.find("th").bind("click.tablesort",function(){e.sort(a(this))});this.direction=this.$th=this.index=null};a.tablesort.prototype={sort:function(d,c){var e=new Date,b=this,g=this.$table,n=0b.value?1*c:a.value
-
-
-
Bad Request
-
The request sent to PuppetDB was invalid. This is usually caused by using an unsupported operator.
-
-
-
+{% block content %}
+Bad Request
+The request sent to PuppetDB was invalid. This is usually caused by using an unsupported operator.
{% endblock %}
diff --git a/puppetboard/templates/403.html b/puppetboard/templates/403.html
index bfe77d0..a5055d3 100644
--- a/puppetboard/templates/403.html
+++ b/puppetboard/templates/403.html
@@ -1,11 +1,5 @@
{% extends 'layout.html' %}
-{% block row_fluid %}
-
-
-
-
Permission Denied
-
What you were looking for has been disabled by the administrator.
-
-
-
+{% block content %}
+Permission Denied
+What you were looking for has been disabled by the administrator.
{% endblock %}
diff --git a/puppetboard/templates/404.html b/puppetboard/templates/404.html
index e45a201..e9a99ac 100644
--- a/puppetboard/templates/404.html
+++ b/puppetboard/templates/404.html
@@ -1,11 +1,5 @@
{% extends 'layout.html' %}
-{% block row_fluid %}
-
-
-
-
Not Found
-
What you were looking for could not be found in PuppetDB.
-
-
-
+{% block content%}
+Not Found
+What you were looking for could not be found in PuppetDB.
{% endblock %}
diff --git a/puppetboard/templates/500.html b/puppetboard/templates/500.html
index b79f451..b35ae0f 100644
--- a/puppetboard/templates/500.html
+++ b/puppetboard/templates/500.html
@@ -1,16 +1,10 @@
{% extends 'layout.html' %}
-{% block row_fluid %}
-
-
-
-
Internal Server Error
-
This error usually occurs because:
-
- We were unable to reach PuppetDB;
- The query to be executed was malformed resulting in an incorrectly encoded request.
-
-
Please have a look at the log output for further information.
-
-
-
+{% block content %}
+Internal Server Error
+This error usually occurs because:
+
+ We were unable to reach PuppetDB;
+ The query to be executed was malformed resulting in an incorrectly encoded request.
+
+Please have a look at the log output for further information.
{% endblock %}
diff --git a/puppetboard/templates/_macros.html b/puppetboard/templates/_macros.html
index ba70a69..3c0bad5 100644
--- a/puppetboard/templates/_macros.html
+++ b/puppetboard/templates/_macros.html
@@ -1,148 +1,148 @@
{% macro facts_table(facts, autofocus=False, condensed=False, show_node=False, show_value=True, link_facts=False, margin_top=20, margin_bottom=20) -%}
-
-
+
+
-
+
{% if show_node %}
Node
{% else %}
Fact
{% endif %}
{% if show_value %}
Value
{% endif %}
{% for fact in facts %}
{% if show_node %}
{{fact.node}}
{% else %}
{{fact.name}}
{% endif %}
{% if show_value %}
{% if link_facts %}
{{fact.value}}
{% else %}
{{fact.value}}
{% endif %}
{% endif %}
{% endfor %}
{%- endmacro %}
{% macro facts_graph(facts, autofocus=False, condensed=False, show_node=False, margin_top=20, margin_bottom=20) -%}
{%- endmacro %}
{% macro facts_graph_value(facts, autofocus=False, condensed=False, show_node=False, margin_top=20, margin_bottom=20) -%}
{%- endmacro %}
{% macro reports_table(reports, nodename, condensed=False, hash_truncate=False, show_conf_col=True, show_agent_col=True, show_host_col=True) -%}
-
+
Only showing the last ten reports.
-
+
Start time
Run time
Full report
{% if show_conf_col %}
Configuration version
{% endif %}
{% if show_agent_col %}
Agent version
{% endif %}
{% if show_host_col %}
Hostname
{% endif %}
{% for report in reports %}
{% if hash_truncate %}
- {% set rep_hash = "%s…"|format(report.hash_[0:6])|safe %}
+ {% set rep_hash = "%s…"|format(report.hash_[0:10])|safe %}
{% else %}
{% set rep_hash = report.hash_ %}
{% endif %}
{% if report.failed %}
{% else %}
{% endif %}
{{report.start}}
{{report.run_time}}
{{rep_hash}}
{% if show_conf_col %}
{{report.version}}
{% endif %}
{% if show_agent_col %}
{{report.agent_version}}
{% endif %}
{% if show_host_col %}
- {{nodename}}
+ {{ report.node }}
{% endif %}
{% endfor %}
{%- endmacro %}
diff --git a/puppetboard/templates/fact.html b/puppetboard/templates/fact.html
index 9950d74..b6a9782 100644
--- a/puppetboard/templates/fact.html
+++ b/puppetboard/templates/fact.html
@@ -1,12 +1,12 @@
{% extends 'layout.html' %}
{% import '_macros.html' as macros %}
{% block content %}
{{name}}{% if value %}/{{value}}{% endif %} ({{facts|length}})
-{{macros.facts_graph(facts, autofocus=True, show_node=True, margin_bottom=10)}}
-{{macros.facts_graph_value(facts, autofocus=True, show_node=True, margin_bottom=10)}}
+{#{{macros.facts_graph(facts, autofocus=True, show_node=True, margin_bottom=10)}}#
+{{macros.facts_graph_value(facts, autofocus=True, show_node=True, margin_bottom=10)}}#}
{% if value %}
{{macros.facts_table(facts, autofocus=True, show_node=True, show_value=False, margin_bottom=10)}}
{% else %}
{{macros.facts_table(facts, autofocus=True, show_node=True, link_facts=True, margin_bottom=10)}}
{% endif %}
{% endblock content %}
diff --git a/puppetboard/templates/facts.html b/puppetboard/templates/facts.html
index 44177ec..98d390c 100644
--- a/puppetboard/templates/facts.html
+++ b/puppetboard/templates/facts.html
@@ -1,16 +1,16 @@
{% extends 'layout.html' %}
{% block content %}
-
-
+
+
{%- for key,facts_list in facts_dict %}
-
{{key}}
+
{{key}}
{%- for fact in facts_list %}
{{fact}}
{%- endfor %}
{% endfor %}
{% endblock content %}
diff --git a/puppetboard/templates/index.html b/puppetboard/templates/index.html
index b144fad..2aac1fd 100644
--- a/puppetboard/templates/index.html
+++ b/puppetboard/templates/index.html
@@ -1,102 +1,106 @@
{% extends 'layout.html' %}
-{% block row_fluid %}
-
-
-
-
-
-
-
+{% block content %}
+
+
+
+
+
+
+
with status failed
+
+
+
+
+
+
with status changed
+
+
+
+
+
+
unreported in the last {{ config.UNRESPONSIVE_HOURS }} hours
-
-
-
-
-
{{metrics['num_nodes']}}
- Population
-
-
-
{{metrics['num_resources']}}
- Resources managed
-
-
-
{{metrics['avg_resources_node']}}
- Avg. resources/node
-
+
+
+
+ Population
+
+
+
+ Resources managed
+
+
+
+ Avg. resources/node
-
-
-
+
+
+
+
{% if nodes %}
-
Nodes status detail ({{nodes|length}})
-
-
-
- Status
- Hostname
-
-
-
-
- {% for node in nodes %}
- {% if node.status != 'unchanged' %}
-
-
-
- {{node.status}}
-
- {% if node.status=='unreported'%}
- {{ node.unreported_time }}
- {% else %}
- {% if node.events['failures'] %}{{node.events['failures']}} {% else %}0 {% endif%}
- {% if node.events['successes'] %}{{node.events['successes']}} {% else %}0 {% endif%}
- {% endif %}
-
- {{ node.name }}
-
- {% if node.unreported_time != None or node.status != 'unreported' %}
- Latest Report
+ Nodes status detail ({{nodes|length}})
+
+
+
+ Status
+ Hostname
+
+
+
+
+ {% for node in nodes %}
+ {% if node.status != 'unchanged' %}
+
+
+
+ {{node.status}}
+
+ {% if node.status=='unreported'%}
+ {{ node.unreported_time }}
{% else %}
- No Report
+ {% if node.events['failures'] %}{{node.events['failures']}} {% else %}0 {% endif%}
+ {% if node.events['successes'] %}{{node.events['successes']}} {% else %}0 {% endif%}
{% endif %}
-
-
- {% endif %}
- {% endfor %}
-
-
+
+
+ {{ node.name }}
+
+
+
+
+
+
+
+ {% endif %}
+ {% endfor %}
+
+
{% else %}
-
Nodes status detail
-
- Nothing seems to be changing.
-
+
Nodes status detail
+
+ Nothing seems to be changing.
+
{% endif %}
-{% endblock row_fluid %}
+{% endblock content %}
diff --git a/puppetboard/templates/layout.html b/puppetboard/templates/layout.html
index a49e942..b7697d4 100644
--- a/puppetboard/templates/layout.html
+++ b/puppetboard/templates/layout.html
@@ -1,67 +1,53 @@
-
Puppetᴃoard
-
-
-
+
Puppetboard
+
+
+
+
-
-
-
-
-
-
-
-
-
Puppetboard
-
-
- {%- for endpoint, caption in [
- ('index', 'Overview'),
- ('nodes', 'Nodes'),
- ('facts', 'Facts'),
- ('reports', 'Reports'),
- ('metrics', 'Metrics'),
- ('query', 'Query'),
- ] %}
- {{ caption }}
- {%- endfor %}
-
-
-
+
- {% block container %}
-
-
- {% block row_fluid %}
-
- {% block content %} {% endblock content %}
-
- {% endblock row_fluid %}
+ {%- for endpoint, caption in [
+ ('index', 'Overview'),
+ ('nodes', 'Nodes'),
+ ('facts', 'Facts'),
+ ('reports', 'Reports'),
+ ('metrics', 'Metrics'),
+ ('query', 'Query'),
+ ] %}
+
{{ caption }}
+ {%- endfor %}
+
+
+
+
+ {% block content %} {% endblock content %}
+
- {% endblock container %}
-
-
+
+
-
-
-
-
+
+
+
+
-
+
+
{% block script %} {% endblock script %}
diff --git a/puppetboard/templates/metric.html b/puppetboard/templates/metric.html
index 0e8c250..1473883 100644
--- a/puppetboard/templates/metric.html
+++ b/puppetboard/templates/metric.html
@@ -1,23 +1,33 @@
{% extends 'layout.html' %}
{% block content %}
-
+
+
+
+ Option
+ Value
+
+
{% for key,value in metric %}
{{key}}
{% if value is mapping %}
{{value|jsonprint}}
{% else %}
{{value}}
{% endif %}
{% endfor %}
{% endblock content %}
diff --git a/puppetboard/templates/node.html b/puppetboard/templates/node.html
index 2a811cb..b5b4519 100644
--- a/puppetboard/templates/node.html
+++ b/puppetboard/templates/node.html
@@ -1,37 +1,39 @@
{% extends 'layout.html' %}
{% import '_macros.html' as macros %}
{% block content %}
-
-
-
Details
-
-
-
- Hostname
- Facts uploaded at
- Catalog compiled at
- Report uploaded at
-
-
-
-
- {{node.name}}
- {{node.facts_timestamp}}
- {{node.catalog_timestamp}}
- {{node.report_timestamp}}
-
-
-
+
+
+
+
Details
+
+
+
+ Hostname
+ {{node.name}}
+
+
+ Facts
+ {{node.facts_timestamp}}
+
+
+ Catalog
+ {{node.catalog_timestamp}}
+
+
+ Report
+ {{node.report_timestamp}}
+
+
+
+
+
+
Reports
+ {{ macros.reports_table(reports, node.name, condensed=True, hash_truncate=True, show_conf_col=False, show_agent_col=False, show_host_col=False)}}
+
-
-
-
+
Facts
- {{macros.facts_table(facts, link_facts=True, condensed=True, margin_top=10)}}
-
-
-
Reports
- {{ macros.reports_table(reports, node.name, condensed=True, hash_truncate=True, show_conf_col=False, show_agent_col=False, show_host_col=False)}}
+ {{macros.facts_table(facts, link_facts=True, condensed=True)}}
{% endblock content %}
diff --git a/puppetboard/templates/nodes.html b/puppetboard/templates/nodes.html
index bc3b6af..2b317e3 100644
--- a/puppetboard/templates/nodes.html
+++ b/puppetboard/templates/nodes.html
@@ -1,60 +1,57 @@
{% extends 'layout.html' %}
{% block content %}
-
- PuppetDB currently only returns active nodes.
+
+
- {% with messages = get_flashed_messages(with_categories=true) %}
- {% if messages %}
- {% for category, message in messages %}
-
- {{message}}
-
- {% endfor %}
- {% endif %}
- {% endwith %}
-
-
-
-
+
Status
Hostname
- Catalog compiled at
- Last report
+ Catalog
+ Report
{% for node in nodes %}
-
- {{ node.status }}
-
- {% if node.status=='unreported'%}
- {{ node.unreported_time }}
- {% else %}
- {% if node.events['failures'] %}{{node.events['failures']}} {% else %}0 {% endif%}
- {% if node.events['successes'] %}{{node.events['successes']}} {% else %}0 {% endif%}
- {% endif %}
+
+
+ {{node.status}}
+
+ {% if node.status=='unreported'%}
+ {{ node.unreported_time }}
+ {% else %}
+ {% if node.events['failures'] %}{{node.events['failures']}} {% else %}0 {% endif%}
+ {% if node.events['successes'] %}{{node.events['successes']}} {% else %}0 {% endif%}
+ {% endif %}
{{node.name}}
{{node.catalog_timestamp}}
{% if node.report_timestamp %}
- {{ node.report_timestamp }}
+ {{ node.report_timestamp }}
{% else %}
-
+
{% endif %}
{% if node.report_timestamp %}
- Latest Report
- Reports
+
+
{% endif %}
{% endfor %}
{% endblock content %}
diff --git a/puppetboard/templates/query.html b/puppetboard/templates/query.html
index dfbe4f0..10089e5 100644
--- a/puppetboard/templates/query.html
+++ b/puppetboard/templates/query.html
@@ -1,61 +1,43 @@
{% extends 'layout.html' %}
-{% block row_fluid %}
-
-
- This is highly experimental and will likely set your server on fire.
-
+{% block content %}
+
Compose
+{% with messages = get_flashed_messages(with_categories=true) %}
+{% if messages %}
+{% for category, message in messages %}
+
+ {{message}}
-
-
-
-
Compose
- {% with messages = get_flashed_messages(with_categories=true) %}
- {% if messages %}
- {% for category, message in messages %}
-
- {{message}}
+{% endfor %}
+{% endif %}
+{% endwith %}
+
+
+
+ {% for subfield in form.endpoints %}
+
+
+ {{subfield }}
+ {{subfield.label}}
+
{% endfor %}
- {% endif %}
- {% endwith %}
-
-
- {% if result %}
-
-
-
Result
+ {{ form.hidden_tag() }}
+
+
+
+
+
+{% if result %}
+
+
+
Result
{{result|jsonprint}}
-
- {% endif %}
-{% endblock row_fluid %}
+{% endif %}
+{% endblock content %}
diff --git a/puppetboard/templates/report.html b/puppetboard/templates/report.html
index 7a452f3..5e4a185 100644
--- a/puppetboard/templates/report.html
+++ b/puppetboard/templates/report.html
@@ -1,73 +1,73 @@
{% extends 'layout.html' %}
{% block content %}
Summary
-
+
Hostname
Configuration version
Start time
End time
{{ report.node }}
{{report.version}}
{{report.start}}
{{report.end}}
Events
-
+
Resource
Status
Changed From
Changed To
{% for event in events %}
{% if not event.failed and event.item['old'] != event.item['new'] %}
-
+
{% elif event.failed %}
-
+
{% endif %}
{{event.item['type']}}[{{event.item['title']}}]
{{event.status}}
{{event.item['old']}}
{{event.item['new']}}
-
+ {#
{{event.item['message']}}
-
+ #}
{% endfor %}
{% endblock content %}
{% block script %}
{% endblock script %}
diff --git a/puppetboard/templates/reports.html b/puppetboard/templates/reports.html
index 2d65c70..924d325 100644
--- a/puppetboard/templates/reports.html
+++ b/puppetboard/templates/reports.html
@@ -1,6 +1,6 @@
{% extends 'layout.html' %}
{% block content %}
-
+
Pending
#21600 . You can access reports for a node or individual reports through the
Nodes tab.
{% endblock content %}
diff --git a/puppetboard/utils.py b/puppetboard/utils.py
index 03ecb85..608fbae 100644
--- a/puppetboard/utils.py
+++ b/puppetboard/utils.py
@@ -1,55 +1,55 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import json
from requests.exceptions import HTTPError, ConnectionError
from pypuppetdb.errors import EmptyResponseError
from flask import abort
def jsonprint(value):
return json.dumps(value, indent=2, separators=(',', ': ') )
def get_or_abort(func, *args, **kwargs):
"""Execute the function with its arguments and handle the possible
errors that might occur.
In this case, if we get an exception we simply abort the request.
"""
try:
return func(*args, **kwargs)
- except HTTPError, e:
+ except HTTPError as e:
abort(e.response.status_code)
except ConnectionError:
abort(500)
except EmptyResponseError:
abort(204)
def ten_reports(reports):
"""Helper to yield the first then reports from the reports generator.
This is an ugly solution at best...
"""
for count, report in enumerate(reports):
if count == 10:
raise StopIteration
yield report
def yield_or_stop(generator):
"""Similar in intent to get_or_abort this helper will iterate over our
generators and handle certain errors.
Since this is also used in streaming responses where we can't just abort
a request we raise StopIteration.
"""
while True:
try:
yield next(generator)
except StopIteration:
raise
except (EmptyResponseError, ConnectionError, HTTPError):
raise StopIteration
diff --git a/requirements.txt b/requirements.txt
index f1c06db..f9f044a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,9 @@
Flask==0.10.1
-Flask-WTF==0.8.4
-Jinja2==2.7
-MarkupSafe==0.18
-WTForms==1.0.4
-Werkzeug==0.9.3
-itsdangerous==0.22
-pypuppetdb==0.1.0
-requests==1.2.3
+Flask-WTF==0.9.4
+Jinja2==2.7.2
+MarkupSafe==0.19
+WTForms==1.0.5
+Werkzeug==0.9.4
+itsdangerous==0.23
+pypuppetdb==0.1.1
+requests==2.2.1
diff --git a/setup.py b/setup.py
index 70a3012..5fc34d4 100644
--- a/setup.py
+++ b/setup.py
@@ -1,49 +1,52 @@
import sys
import os
import codecs
from setuptools import setup, find_packages
if sys.argv[-1] == 'publish':
os.system('python setup.py sdist upload')
sys.exit()
VERSION = "0.0.4"
with codecs.open('README.rst', encoding='utf-8') as f:
README = f.read()
with codecs.open('CHANGELOG.rst', encoding='utf-8') as f:
CHANGELOG = f.read()
setup(
name='puppetboard',
version=VERSION,
author='Daniele Sluijters',
author_email='daniele.sluijters+pypi@gmail.com',
packages=find_packages(),
url='https://github.com/nedap/puppetboard',
license='Apache License 2.0',
description='Web frontend for PuppetDB',
include_package_data=True,
long_description='\n'.join((README, CHANGELOG)),
install_requires=[
"Flask >= 0.10.1",
"Flask-WTF >= 0.9.4",
"pypuppetdb >= 0.1.0",
],
keywords="puppet puppetdb puppetboard",
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Web Environment',
'Framework :: Flask',
'Intended Audience :: System Administrators',
'Natural Language :: English',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
],
)